﻿/* 
* Copyright (c) 2010, The University of Texas at Austin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, 
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, 
* this list of conditions and the following disclaimer in the documentation and/or other materials 
* provided with the distribution.
*
* Neither the name of The University of Texas at Austin nor the names of its contributors may be 
* used to endorse or promote products derived from this software without specific prior written 
* permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Bio.Data;
using Bio.Data.Interfaces;
using Bio.Data.Providers.Structure;
using LabelRNA;

namespace Bio.Views.Structure.Editor.Internal.Data
{
    public class EditableStructureModel : IStructureModelBioEntity
    {
        public Dictionary<int, BasePairMetadata> BasePairPropertyEdits { get; private set; }
        public Dictionary<int, NucleotideMetadata> NucleotidePropertyEdits { get; private set; }
        public StructureModelMetadata StructureModelPropertyEdits { get; set; }
        public StructureModelMetadata StructureModelProperties { get; set; }
        public SequenceRendering SequenceRenderingEdits { get; set; }

        public IBioSequence Sequence { get { return Data.Sequence; } }
        public IList<IStructureExtentBioEntity> Extents { get { return _localExtents; } }
        public IList<IBasePairBioEntity> BasePairs { get { return _localBasePairs; } }

        public bool ModelIsDirty
        {
            get
            {
                return (PairingsEditStack.Count > 0) ? true : false;
            }
        }

        public void Reset()
        {
            IExtensibleProperties metadata = Data as IExtensibleProperties;
            if (metadata != null && metadata.DoesExtendedPropertyExist(StructureModelMetadata.Key))
                StructureModelProperties = metadata.GetExtendedProperty<StructureModelMetadata>(StructureModelMetadata.Key);

            _localBasePairs = new List<IBasePairBioEntity>();
            _localBasePairs.AddRange(Data.BasePairs);

            _localExtents = new List<IStructureExtentBioEntity>();
            _localExtents.AddRange(Data.Extents);

            PairingsEditStack.Clear();
        }

        public void UndoLastModelChange()
        {
            if (PairingsEditStack.Count <= 0)
                return;

            List<StructureModelEdit> lastEditFrame = PairingsEditStack.Pop();
            foreach (var edit in lastEditFrame)
            {
                if (edit.EditType == StructureModelEdit.ModelEditType.Add)
                {
                    if (_localBasePairs.Contains(edit.BasePair))
                    {
                        if (BasePairPropertyEdits.ContainsKey(edit.BasePair.FivePrimeIndex))
                        {
                            BasePairPropertyEdits.Remove(edit.BasePair.FivePrimeIndex);
                        }
                        _localBasePairs.Remove(edit.BasePair);
                    }
                }
                else if (edit.EditType == StructureModelEdit.ModelEditType.Delete)
                {
                    if (!_localBasePairs.Contains(edit.BasePair))
                    {
                        _localBasePairs.Add(edit.BasePair);
                    }
                }
            }
            RecomputeExtents();
        }

        /// <summary>
        /// Pass a set of new pairings as key, value pairs. It is assummed that the five prime part of the pairing is the key and
        /// the three prime part of the pairing is the value.
        /// </summary>
        /// <param name="newPairings"></param>
        /// <param name="force"></param>
        /// <returns></returns>
        public bool AddBasePairs(List<KeyValuePair<int, int>> newPairings, bool force)
        {
            if (newPairings == null)
                return false;

            var invalidpairings = newPairings.Where(np => (np.Key < 0 || 
                np.Key > (Sequence.SequenceData.Count - 1) ||
                np.Value > (Sequence.SequenceData.Count - 1) ||
                np.Key == np.Value ||
                np.Value < np.Key));

            if (invalidpairings.Count() > 0)
                return false;

            List<StructureModelEdit> edits = new List<StructureModelEdit>();
            foreach (var np in newPairings)
            {
                var existingBp = _localBasePairs.Where(bp => bp.FivePrimeIndex == np.Key && bp.ThreePrimeIndex == np.Value).FirstOrDefault();
                if (existingBp != null)
                    continue; //No work to do for this pairing.

                var existingBpWithDesiredFPIdx = _localBasePairs.Where(bp => bp.FivePrimeIndex == np.Key || bp.ThreePrimeIndex == np.Key);
                var existingBpWithDesiredTPIdx = _localBasePairs.Where(bp => bp.FivePrimeIndex == np.Value || bp.ThreePrimeIndex == np.Value);

                if ((existingBpWithDesiredFPIdx.Count() > 0 && !force) || (existingBpWithDesiredTPIdx.Count() > 0 && !force))
                    return false;

                //1. Clear the existing conflicts.
                List<int> basePairConflicts = existingBpWithDesiredFPIdx.Union(existingBpWithDesiredTPIdx).Select(bp => bp.FivePrimeIndex).ToList<int>();
                List<StructureModelEdit> editsForConflicts = null;
                if (basePairConflicts.Count > 0)
                {
                    editsForConflicts = RemoveBasePairsInternal(basePairConflicts, false);
                    if (editsForConflicts == null)
                        return false;
                    edits.AddRange(editsForConflicts); //Consolidate the conflicts into the edit frame.
                }

                //2. Create the new pairing.
                IBasePairBioEntity newBasePair = new RNASecondaryStructureBasePair(Sequence, np.Key, np.Value);
                edits.Add(new StructureModelEdit() { EditType = StructureModelEdit.ModelEditType.Add, BasePair = newBasePair });
                _localBasePairs.Add(newBasePair);
            }

            if (edits.Count > 0)
                PairingsEditStack.Push(edits);

            RecomputeExtents();
            return true;
        }

        /// <summary>
        /// If the force flag is set, and either nucleotide is in an existing pairing, those pairings will be deleted first.
        /// </summary>
        /// <param name="fivePrimeIdx"></param>
        /// <param name="threePrimeIdx"></param>
        /// <param name="force"></param>
        /// <returns></returns>
        public bool AddBasePair(int fivePrimeIdx, int threePrimeIdx, bool force)
        {
            if (fivePrimeIdx < 0 || fivePrimeIdx > (Sequence.SequenceData.Count - 1))
                return false;

            if (threePrimeIdx <= fivePrimeIdx || threePrimeIdx > (Sequence.SequenceData.Count - 1))
                return false;

            //Check is the selected base pair already exists or if the two indices are in other pairs.
            var existingBp = _localBasePairs.Where(bp => bp.FivePrimeIndex == fivePrimeIdx && bp.ThreePrimeIndex == threePrimeIdx).FirstOrDefault();
            if (existingBp != null)
                return true; //No work to do

            var existingBpWithDesiredFPIdx = _localBasePairs.Where(bp => bp.FivePrimeIndex == fivePrimeIdx || bp.ThreePrimeIndex == threePrimeIdx);
            var existingBpWithDesiredTPIdx = _localBasePairs.Where(bp => bp.FivePrimeIndex == threePrimeIdx || bp.ThreePrimeIndex == threePrimeIdx);

            if ((existingBpWithDesiredFPIdx.Count() > 0 && !force) || (existingBpWithDesiredTPIdx.Count() > 0 && !force))
                return false;

            //1. Clear the existing conflicts.
            List<int> basePairConflicts = existingBpWithDesiredFPIdx.Union(existingBpWithDesiredTPIdx).Select(bp => bp.FivePrimeIndex).ToList<int>();
            List<StructureModelEdit> editsForConflicts = null;
            if (basePairConflicts.Count > 0)
            {
                editsForConflicts = RemoveBasePairsInternal(basePairConflicts, false);
                if (editsForConflicts == null)
                    return false; //We encountered some error removing conflicts.
            }

            //2. Create the new pairing.
            IBasePairBioEntity newBasePair = new RNASecondaryStructureBasePair(Sequence, fivePrimeIdx, threePrimeIdx);
            StructureModelEdit edit = new StructureModelEdit() { EditType = StructureModelEdit.ModelEditType.Add, BasePair = newBasePair  };
            _localBasePairs.Add(newBasePair);

            if (editsForConflicts != null && editsForConflicts.Count > 0) 
            {
                editsForConflicts.Add(edit); //The edit frame is the conflits + the new base pair.
                PairingsEditStack.Push(editsForConflicts);
            }
            else
                PairingsEditStack.Push(new List<StructureModelEdit>() { edit });

            RecomputeExtents();
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="basePairsByFivePrimeIdx"></param>
        /// <returns></returns>
        public bool RemoveBasePairs(IEnumerable<int> basePairsByFivePrimeIdx)
        {
            List<StructureModelEdit> edits = RemoveBasePairsInternal(basePairsByFivePrimeIdx, true);
            if (edits != null)
            {
                PairingsEditStack.Push(edits);
                return true;
            }
            else { return false; } //We couldn't resolve all the base pairs to be deleted.
        }

        public bool RemoveBasePair(int fivePrimeIdx)
        {
            //1. We remove the base pair from the local list (we don't touch the data)
            //2. We remove the base pair edits if any have been applied to that base pair
            //3. Put create an edit
            //3. We re-compute the Extents
            //4. We would have to dump extent edits for an extents that are removed.
            var localBasePairEntity = _localBasePairs.FirstOrDefault(bp => bp.FivePrimeIndex == fivePrimeIdx);
            if (localBasePairEntity != null)
            {
                StructureModelEdit edit = new StructureModelEdit() { EditType = StructureModelEdit.ModelEditType.Delete, BasePair = localBasePairEntity };
                if (BasePairPropertyEdits.ContainsKey(fivePrimeIdx))
                {
                    BasePairPropertyEdits.Remove(fivePrimeIdx);
                }

                PairingsEditStack.Push(new List<StructureModelEdit>() { edit });

                _localBasePairs.Remove(localBasePairEntity);
                RecomputeExtents(); //If this fails, we might want to think about how to notify the user that higher order elements were lost.
                return true;
            }
            return false; //We couldn't find the base pair that was supposedly deleted.
        }

        public EditableStructureModel(IStructureModelBioEntity data)
        {
            Debug.Assert(data != null);
            Data = data;
            BasePairPropertyEdits = new Dictionary<int, BasePairMetadata>();
            NucleotidePropertyEdits = new Dictionary<int, NucleotideMetadata>();
            PairingsEditStack = new Stack<List<StructureModelEdit>>();
            //StructureModelPropertyEdits = null;
            Reset();
        }

        private IStructureModelBioEntity Data { get; set; }
        private Stack<List<StructureModelEdit>> PairingsEditStack { get; set; }
        private List<IBasePairBioEntity> _localBasePairs;
        private List<IStructureExtentBioEntity> _localExtents;

        private List<StructureModelEdit> RemoveBasePairsInternal(IEnumerable<int> basePairsByFivePrimeIdx, bool recomputeExtents)
        {
            if (basePairsByFivePrimeIdx == null)
                throw new ArgumentException(string.Format("Error: Passed an invalid argument to RemoveBasePairs"));

            var localBasePairEntities = from bp in _localBasePairs
                                        from bpFPIdx in basePairsByFivePrimeIdx
                                        where bp.FivePrimeIndex == bpFPIdx
                                        select bp;
            List<IBasePairBioEntity> localBasePairsToRemove = localBasePairEntities.ToList<IBasePairBioEntity>();
            List<StructureModelEdit> edits = new List<StructureModelEdit>();
            if (localBasePairsToRemove.Count > 0)
            {
                foreach (var localBasePairEntity in localBasePairsToRemove)
                {
                    StructureModelEdit edit = new StructureModelEdit() { EditType = StructureModelEdit.ModelEditType.Delete, BasePair = localBasePairEntity };
                    if (BasePairPropertyEdits.ContainsKey(localBasePairEntity.FivePrimeIndex))
                    {
                        BasePairPropertyEdits.Remove(localBasePairEntity.FivePrimeIndex);
                    }
                    edits.Add(edit);

                    _localBasePairs.Remove(localBasePairEntity);
                }
                if(recomputeExtents)
                    RecomputeExtents();
                return edits;
            }
            else { return null; } //We couldn't resolve all the base pairs to be deleted.
        }


        private bool RecomputeExtents()
        {
            _localBasePairs = _localBasePairs.OrderBy(bp => bp.FivePrimeIndex).ToList();
            Dictionary<int, int> pairMap = new Dictionary<int, int>();
            foreach (var bp in _localBasePairs)
                pairMap.Add(bp.FivePrimeIndex, bp.ThreePrimeIndex);

            RNAParser parser = new RNAParser(pairMap, Sequence.SequenceData.Count);
            try
            {
                XElement extentsXML = parser.ParseToExtents();
                var extents = from extent in extentsXML.Descendants("Extent")
                              select new RNASecondaryStructureExtent(Sequence,
                                                                   Int32.Parse(extent.Element("ExtentID").Value),
                                                                   Int32.Parse(extent.Element("ExtentOrdinal").Value),
                                                                   (StructureExtentType)Enum.Parse(typeof(StructureExtentType), extent.Element("ExtentTypeID").Value, true),
                                                                   Int32.Parse(extent.Element("ExtentStartIndex").Value),
                                                                   Int32.Parse(extent.Element("ExtentEndIndex").Value));
                //Now, we need to compare the extents to figure out which were changed.
                var sameExtents = _localExtents.Intersect(extents, new ExtentComparer());
                var deletedExtents = _localExtents.Except(extents, new ExtentComparer());
                var addedExtents = extents.Except(sameExtents, new ExtentComparer());
                _localExtents = extents.ToList<IStructureExtentBioEntity>();
                return true;
            }
            catch (Exception)
            {
                _localExtents.Clear();
                return false;
            }
        }

        //This is an internal comparer, so it already "knows" that the sequence is the same and we can "casually" compare the extents.
        class ExtentComparer : IEqualityComparer<IStructureExtentBioEntity>
        {
            #region IEqualityComparer<IStructureExtentBioEntity> Members

            public bool Equals(IStructureExtentBioEntity x, IStructureExtentBioEntity y)
            {
                return (x != null &&
                        y != null &&
                        x.Type == y.Type &&
                        x.StartIndex == y.StartIndex &&
                        x.EndIndex == y.EndIndex) ? true : false;
            }

            public int GetHashCode(IStructureExtentBioEntity obj)
            {
                return base.GetHashCode();
            }

            #endregion
        }

        class StructureModelEdit
        {
            public enum ModelEditType { Delete, Add };
            public ModelEditType EditType { get; set; }
            public IBasePairBioEntity BasePair { get; set; }
        }
    }
}
